﻿namespace Jsl.FxCop
{
    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// A utility class to make working with FxCop CCI easier.
    /// </summary>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Cci", Justification = "Matches Microsoft's name.")]
    public static class CciHelper
    {
        #region Public Methods
        /// <summary>
        /// Expands the <see cref="Block"/> into its statements.
        /// </summary>
        /// <param name="block">The <see cref="Block"/> to expand.</param>
        /// <param name="includeBlocks">If <c>true</c> then include <see cref="Block"/>s in enumeration.</param>
        /// <returns>All the statements in the block and any contained blocks.</returns>
        public static IEnumerable<Statement> ExpandBlocks(this Block block, bool includeBlocks)
        {
            return block.Statements.ExpandBlocks(includeBlocks);
        }

        /// <summary>
        /// Expands any <see cref="Block"/>s in the statements to their contained statements.
        /// </summary>
        /// <param name="statements">The statements to expand the blocks in.</param>
        /// <param name="includeBlocks">If <c>true</c> then include <see cref="Block"/>s in enumeration.</param>
        /// <returns>All the statements with any blocks expanded.</returns>
        public static IEnumerable<Statement> ExpandBlocks(this IEnumerable<Statement> statements, bool includeBlocks)
        {
            foreach (var statement in statements)
            {
                if (statement.NodeType == NodeType.Block)
                {
                    var block = (Block)statement;

                    if (includeBlocks)
                    {
                        yield return block;
                    }

                    foreach (Statement childStatement in block.Statements.ExpandBlocks(includeBlocks))
                    {
                        yield return childStatement;
                    }
                }
                else
                {
                    yield return statement;
                }
            }
        }

        /// <summary>
        /// Gets the field if the expression is a field.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <returns>The field if the expression is a field.</returns>
        public static Field GetField(this Expression expression)
        {
            var binding = expression as MemberBinding;
            if (binding != null)
            {
                return binding.BoundMember as Field;
            }

            return null;
        }

        /// <summary>
        /// Gets the field from simple getter.
        /// </summary>
        /// <param name="getterMethod">The method to check.</param>
        /// <returns>The field from simple getter.</returns>
        /// <remarks>
        /// Checks all the return statements from the specified method. If all the return statements simply return the
        /// same field, then it is assumed that this is a simple getter for a specific field. That field is returned.
        /// </remarks>
        public static Field GetFieldFromSimpleGetter(this Method getterMethod)
        {
            var methodFlow = new StatementFlow(getterMethod);

            var returnStatements = methodFlow.Statements.OfType<ReturnNode>();

            var returnedFields =
                from returnStatement in returnStatements
                select returnStatement.Expression.GetResolvedThisField();

            return returnedFields.Distinct().SingleOrDefault();
        }

        /// <summary>
        /// Gets the method called.
        /// </summary>
        /// <param name="call">The method call.</param>
        /// <returns>The method called.</returns>
        public static Method GetMethod(this MethodCall call)
        {
            var callee = call.Callee as MemberBinding;
            if (callee == null)
            {
                return null;
            }

            return callee.BoundMember as Method;
        }

        /// <summary>
        /// Gets the method call from the expression if it is one.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <returns>The <see cref="MethodCall"/> or <c>null</c>.</returns>
        public static MethodCall GetMethodCall(this Expression expression)
        {
            var binding = expression as MemberBinding;
            if (binding != null)
            {
                return binding.TargetObject as MethodCall;
            }

            return null;
        }

        /// <summary>
        /// Gets the resolved This field.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <returns>The resolved This field.</returns>
        /// <remarks>
        /// This is used when you want to get the specified field if the expression is the field or a simple property getter
        /// that gets the field.
        /// Note that this method uses flow analysis to try to determine if a method is a simple property getter and may be
        /// expensive on large methods.
        /// </remarks>
        public static Field GetResolvedThisField(this Expression expression)
        {
            var binding = expression as MemberBinding;
            if ((binding != null)
                && (binding.TargetObject != null)
                && (binding.TargetObject.NodeType == NodeType.This))
            {
                if (binding.BoundMember.NodeType == NodeType.Field)
                {
                    return (Field)binding.BoundMember;
                }
                else if (binding.BoundMember.NodeType == NodeType.Method)
                {
                    var method = (Method)binding.BoundMember;
                    return method.GetFieldFromSimpleGetter();
                }
            }

            return null;
        }

        /// <summary>
        /// Gets the simple expression.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <returns>The expression if it was simple enough, <c>null</c> otherwise.</returns>
        /// <remarks>
        /// A simple expression is This, a local or a field in This. All others are returned as <c>null</c>.
        /// </remarks>
        public static Node GetSimpleExpression(this Expression expression)
        {
            if (expression == null)
            {
                return null;
            }

            switch (expression.NodeType)
            {
                case NodeType.Local:
                case NodeType.This:
                    return expression;

                default:
                    return expression.GetThisField();
            }
        }

        /// <summary>
        /// Gets the target object of the call.
        /// </summary>
        /// <param name="call">The call to use.</param>
        /// <returns>The target object or <c>null</c>.</returns>
        public static Expression GetTargetObject(this MethodCall call)
        {
            var binding = call.Callee as MemberBinding;
            if (binding != null)
            {
                return binding.TargetObject;
            }

            return null;
        }

        /// <summary>
        /// Gets the field from "this" instance.
        /// </summary>
        /// <param name="expression">The expression to get the field from.</param>
        /// <returns>The field if it is one and is from "this" instance.</returns>
        public static Field GetThisField(this Expression expression)
        {
            var binding = expression as MemberBinding;
            if (binding != null)
            {
                // The TargetObject is null for expressions like: System.Drawing.Color.Empty
                if (binding.TargetObject != null)
                {
                    if (binding.TargetObject.NodeType == NodeType.This)
                    {
                        if (binding.BoundMember.NodeType == NodeType.Field)
                        {
                            return (Field)binding.BoundMember;
                        }
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Filters the Nop statements from the list of statement.
        /// </summary>
        /// <param name="statements">The statements to filter.</param>
        /// <returns>The statements without any Nop statements.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Nops", Justification = "This is the No Operation instruction.")]
        public static IEnumerable<Statement> IgnoreNops(this IEnumerable<Statement> statements)
        {
            return
                from statement in statements
                where statement.NodeType != NodeType.Nop
                select statement;
        }

        /// <summary>
        /// Determines whether the <see cref="MethodCall"/> is a call to the specified method.
        /// </summary>
        /// <param name="call">The method call to process.</param>
        /// <param name="typeName">Name of the type.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <returns><c>true</c> if the <see cref="MethodCall"/> is a call to the specified method.</returns>
        public static bool IsCallTo(this MethodCall call, string typeName, string methodName)
        {
            var method = call.GetMethod();
            if (method == null)
            {
                return false;
            }

            return (method.DeclaringType.FullName == typeName)
                && (method.Name.Name == methodName);
        }

        /// <summary>
        /// Tries the get the value of the <see cref="Font"/>.
        /// </summary>
        /// <param name="call">The method call.</param>
        /// <param name="font">The font if found.</param>
        /// <returns><c>true</c> if the font was found.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "MethodCall is the only expression this makes sense for.")]
        public static bool TryGetFont(this MethodCall call, out Font font)
        {
            // Assume we won't be able to get the font.
            font = null;

            // We are expecting a Font constructor so exit if not found.
            var fontConstructor = call.Operands[0] as Construct;
            if (fontConstructor == null)
            {
                return false;
            }

            if (!CciHelper.IsAppropriateFontConstructor(fontConstructor))
            {
                return false;
            }

            // Get the font's family name.
            string familyName;
            if (!fontConstructor.Operands[0].TryGetLiteral(out familyName))
            {
                return false;
            }

            // Get the font size.
            float fontSize;
            if (!fontConstructor.Operands[1].TryGetLiteral(out fontSize))
            {
                return false;
            }

            // Build the Font.
            if (fontConstructor.Operands.Count == 2)
            {
                font = new Font(familyName, fontSize);
            }
            else
            {
                // The FontStyle is passed as an Int32.
                int intValue;
                if (!fontConstructor.Operands[2].TryGetLiteral(out intValue))
                {
                    return false;
                }

                font = new Font(familyName, fontSize, (FontStyle)intValue);
            }

            return true;
        }

        /// <summary>
        /// Tries the get the value of the literal.
        /// </summary>
        /// <typeparam name="TValue">The type of the value.</typeparam>
        /// <param name="expression">The expression.</param>
        /// <param name="value">The value of the literal.</param>
        /// <returns><c>true</c> if the literal was of the appropriate type.</returns>
        public static bool TryGetLiteral<TValue>(this Expression expression, out TValue value)
        {
            var literal = expression as Literal;
            if (literal != null)
            {
                if (IsAppropriateType<TValue>(literal))
                {
                    value = (TValue)literal.Value;
                    return true;
                }
            }

            value = default(TValue);
            return false;
        }

        /// <summary>
        /// Tries the get <see cref="Point"/> constant.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <param name="value">The value.</param>
        /// <returns>The <see cref="Point"/> if found and is a constant.</returns>
        public static bool TryGetPoint(this Expression expression, out Point value)
        {
            value = Point.Empty;

            var pointConstructor = expression as Construct;
            if ((pointConstructor == null) || (pointConstructor.Type.FullName != WellKnownTypeNames.Point))
            {
                return false;
            }

            if (pointConstructor.Operands.Count != 2)
            {
                return false;
            }

            // Try to get the x and y literal integers.
            int x;
            int y;
            if (!pointConstructor.Operands[0].TryGetLiteral(out x)
                || !pointConstructor.Operands[1].TryGetLiteral(out y))
            {
                return false;
            }

            value = new Point(x, y);
            return true;
        }

        /// <summary>
        /// Tries the get <see cref="Size"/> constant.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <param name="value">The value.</param>
        /// <returns>The <see cref="Size"/> if found and is a constant.</returns>
        public static bool TryGetSize(this Expression expression, out Size value)
        {
            value = Size.Empty;

            var sizeConstructor = expression as Construct;
            if ((sizeConstructor == null) || (sizeConstructor.Type.FullName != WellKnownTypeNames.Size))
            {
                return false;
            }

            if (sizeConstructor.Operands.Count != 2)
            {
                return false;
            }

            // Try to get the width and height literal integers.
            int width;
            int height;
            if (!sizeConstructor.Operands[0].TryGetLiteral(out width)
                || !sizeConstructor.Operands[1].TryGetLiteral(out height))
            {
                return false;
            }

            value = new Size(width, height);
            return true;
        }

        /// <summary>
        /// Tries the get <see cref="SizeF"/> constant.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <param name="value">The value.</param>
        /// <returns>The <see cref="SizeF"/> if found and is a constant.</returns>
        public static bool TryGetSizeF(this Expression expression, out SizeF value)
        {
            value = SizeF.Empty;

            var sizeConstructor = expression as Construct;
            if ((sizeConstructor == null) || (sizeConstructor.Type.FullName != WellKnownTypeNames.SizeF))
            {
                return false;
            }

            if (sizeConstructor.Operands.Count != 2)
            {
                return false;
            }

            // Try to get the width and height literal floats.
            float width;
            float height;
            if (!sizeConstructor.Operands[0].TryGetLiteral(out width)
                || !sizeConstructor.Operands[1].TryGetLiteral(out height))
            {
                return false;
            }

            value = new SizeF(width, height);
            return true;
        }
        #endregion Public Methods

        #region Private Methods
        /// <summary>
        /// Determines if the font constructor is appropriate for processing.
        /// </summary>
        /// <param name="fontConstructor">The font constructor.</param>
        /// <returns><c>true</c> if the constructor is appropriate.</returns>
        /// <remarks>
        /// The only constructors we process are Font(string, single) and Font(string, single, FontStyle).
        /// </remarks>
        private static bool IsAppropriateFontConstructor(Construct fontConstructor)
        {
            if (fontConstructor.Type.FullName == WellKnownTypeNames.Font)
            {
                if ((fontConstructor.Operands.Count == 2) || (fontConstructor.Operands.Count == 3))
                {
                    if (fontConstructor.Operands[0].Type == FrameworkTypes.String)
                    {
                        if (fontConstructor.Operands[1].Type == FrameworkTypes.Single)
                        {
                            return (fontConstructor.Operands.Count == 2)
                                || (fontConstructor.Operands[2].Type.FullName == WellKnownTypeNames.Int32);
                        }
                    }
                }
            }

            return false;
        }

        private static bool IsAppropriateType<TValue>(Literal literal)
        {
            var literalType = literal.Type;
            return ((typeof(TValue) == typeof(int)) && (literalType == FrameworkTypes.Int32))
                || ((typeof(TValue) == typeof(float)) && (literalType == FrameworkTypes.Single))
                || ((typeof(TValue) == typeof(double)) && (literalType == FrameworkTypes.Double))
                || ((typeof(TValue) == typeof(string)) && (literalType == FrameworkTypes.String));
        }
        #endregion Private Methods
    }
}